package com.naiterui.ehp.bs.weixin.security.config;

import java.io.IOException;
import java.util.Date;
import java.util.Optional;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.filter.OncePerRequestFilter;

import com.google.common.base.Charsets;
import com.naiterui.ehp.bp.support.security.SettingConfig;
import com.naiterui.ehp.bs.weixin.exception.WeixinException;
import com.naiterui.ehp.bs.weixin.security.api.JwtApi;
import com.naiterui.ehp.bs.weixin.security.api.JwtService;
import com.naiterui.ehp.bs.weixin.security.bean.JwtInfo;
import com.naiterui.ehp.bs.weixin.security.bean.JwtUser;
import com.naiterui.ehp.bs.weixin.security.properties.JwtProperties;
import com.naiterui.ehp.bs.weixin.security.util.JwtTokenUtil;
import com.naiterui.ehp.bs.weixin.security.util.JwtUserUtil;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;


@Slf4j
// @Configuration
@EnableConfigurationProperties(JwtProperties.class)
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class JwtConfig extends WebSecurityConfigurerAdapter {

    private final JwtProperties jwtProperties;
    private final ApplicationContext applicationContext;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        /**
         * 在 UsernamePasswordAuthenticationFilter 之前添加 JwtAuthenticationTokenFilter
         */
        http.addFilterBefore(this.tokenFilter(), UsernamePasswordAuthenticationFilter.class);

        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().authorizeRequests()
            .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
            .anyRequest().authenticated()   // 任何请求,登录后可以访问
            .and().headers().cacheControl();

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
        //让Spring security 放行所有preflight request（cors 预检请求）
        //registry.requestMatchers(CorsUtils::isPreFlightRequest).permitAll();
        // 处理异常情况：认证失败和权限不足
        http.exceptionHandling().authenticationEntryPoint(this.unAuthPoint());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
            .antMatchers(this.jwtProperties.getAuthPath())
            .and()
            .ignoring().antMatchers("/actuator/health")
            .and()
            .ignoring()
            .antMatchers(Optional.ofNullable(this.jwtProperties.getAuthIgnore()).orElse(new String[]{}))
            .antMatchers(this.defaultAuthenticationIgnoringPath());
    }

    private String[] defaultAuthenticationIgnoringPath() {
        return new String[]{
            "/",
            "/echo.jsp",
            "/doc.html",
            "/static/**",
            "/v2/api-docs/**",
            "/com.mysql.cj.jdbc.Driver",
            "/swagger-resources/**",
            "/webjars/**",
            "/favicon.ico",
            "/inner/wechat/miniapp/qrcode"};
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
        CorsConfiguration cors = new CorsConfiguration();
        cors.setAllowCredentials(true);
        cors.addAllowedOrigin("*");
        cors.addAllowedHeader("*");
        cors.addAllowedMethod("*");
        configurationSource.registerCorsConfiguration("/**", cors);
        return new CorsFilter(configurationSource);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 默认用户信息获取，业务服务根据用户逻辑实现db查询，userName统一为userId（Long）
     *
     * @return
     */
    @Override
    @Bean
    public UserDetailsService userDetailsService() {

        return username -> {
            JwtUser jwtUser = applicationContext.getBean(JwtService.class).loadUser(Long.parseLong(username));
            return jwtUser;
        };

    }

    /**
     * 默认用户密码encoder，业务服务根据密码加密逻辑实现，基于jwt，默认全通过
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public PasswordEncoder passwordEncoder() {

        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return (String) charSequence;
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return true;
            }
        };
    }


    @Bean
    public OncePerRequestFilter tokenFilter() {
        return new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
                String token = request.getHeader(SettingConfig.TOKEN_HEADER);
                if (!StringUtils.isEmpty(token) && !JwtTokenUtil.isTokenExpired(token, jwtProperties.getSecret())) {
                    String username = JwtTokenUtil.getUserNameFromToken(token, jwtProperties.getSecret());
                    JwtUser jwtUser = (JwtUser) JwtConfig.this.userDetailsService().loadUserByUsername(username);
                    if (jwtUser != null && SecurityContextHolder.getContext().getAuthentication() == null && JwtTokenUtil
                        .validateToken(token, jwtProperties.getSecret(), jwtUser)) {
                        jwtUser = JwtTokenUtil.populateUser(token, jwtProperties.getSecret());
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(jwtUser, null,
                            jwtUser.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
                filterChain.doFilter(request, response);
            }
        };
    }

    @Bean
    public AuthenticationEntryPoint unAuthPoint() {
        return (httpServletRequest, httpServletResponse, e) -> {

            log.error("auth failed ; error={}", e.getMessage());

            httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            httpServletResponse.setCharacterEncoding(Charsets.UTF_8.name());
            httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            //httpServletResponse.getWriter().print(PublicService.returnResponseVO(BaseExceptionCodes.TOLOGIN).toString());
            httpServletResponse.getWriter().print("{\"code\":1,\"msg\":\"您还没有登录，请登录\"}");
        };
    }

    @Bean
    public JwtApi jwtApi() {
        return new JwtApi() {
            @Override
            public JwtInfo generateJwt(Long userId) {
                log.info("Jwtapi generateJwt");
                if (userId == null) {
                    return null;
                }
                String userName = userId.toString();
                try {
                    JwtConfig.this.authenticationManagerBean().authenticate(new UsernamePasswordAuthenticationToken(userName, userName));
                } catch (Exception e) {
                    throw new RuntimeException("");
                }
                // 获取用户
                JwtUser jwtUser = (JwtUser) JwtConfig.this.userDetailsService().loadUserByUsername(userName);
                //生成token
                String token = JwtTokenUtil.generateToken(jwtUser, jwtProperties.getSecret(), jwtProperties.getExpire());
                jwtUser.setToken(token);
                // Return the token
                return new JwtInfo(SettingConfig.TOKEN_HEADER, token);
            }

            @Override
            public JwtInfo refreshJwt(String token) throws WeixinException {
                log.info("Jwtapi refreshJwt");
                JwtUser jwtUser = JwtUserUtil.getJwtUser();
                if (JwtTokenUtil.validateToken(jwtUser.getToken(), jwtProperties.getSecret(), jwtUser)) {
                    String refreshToken = JwtTokenUtil.refreshToken(jwtUser.getToken(), jwtProperties.getSecret(), jwtProperties.getExpire());
                    return new JwtInfo(SettingConfig.TOKEN_HEADER, refreshToken);
                }
                return null;
            }
        };
    }

    @Bean
    @ConditionalOnMissingBean
    public JwtService jwtService() {

        return userId -> new JwtUser(JwtUser.USER_TYPE_PATIENT, userId, new Date());
    }

}
